/**
 * Copyright 2012 ibiblio
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.ibiblio.terasaur.bt

import org.ibiblio.terasaur.bt.Torrent
import org.ibiblio.terasaur.bencode.BencodeException
import org.ibiblio.terasaur.bt.TorrentServiceException

import org.springframework.web.multipart.MultipartHttpServletRequest
import org.springframework.web.multipart.commons.CommonsMultipartFile
import org.springframework.web.multipart.MultipartFile
import javax.servlet.http.HttpServletResponse
import grails.plugins.springsecurity.Secured
import org.ibiblio.terasaur.bt.TorrentFileManager
import org.ibiblio.terasaur.bt.TorrentMetainfo
import org.ibiblio.terasaur.item.Item
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormatter
import org.ibiblio.terasaur.search.DateHelper
import org.ibiblio.terasaur.ui.UrlHelper
import org.ibiblio.terasaur.item.PermissionHelper
import org.ibiblio.terasaur.controller.ItemMixin
import org.ibiblio.terasaur.controller.TorrentMixin
import javax.servlet.http.HttpServletResponse
import org.springframework.security.access.AccessDeniedException
import org.grails.plugins.springsecurity.service.acl.AclUtilService

class TorrentControllerException extends Exception {
    public TorrentControllerException(String msg) {
        super(msg)
    }
}

@Mixin([ItemMixin, TorrentMixin])
class TorrentController {
    static defaultAction = "index"
    protected TorrentFileManager _torrentFileManager = null
    def torrentService // injected
    def springSecurityService // injected
    def aclUtilService // injected
    static UrlHelper urlHelper = new UrlHelper()
    static PermissionHelper permissionHelper = new PermissionHelper()

    // TODO: move to config defaults file
    static _torrentUploadName = "torrentFile" // the file input field name in the upload form

    /**
     * Torrents can be ed, but not created
     */
    def create = {
        redirect(controller:"browse", action: "recent")
    }

    /**
     * Show torrent details
     *
     * check for valid info hash
     * if not, return not found
     * check that torrent exists
     * if not, return not found
     */
    def show = {
        Torrent torrent = getTorrentInstance()
        if (!torrent) {
            this._setTorrentNotFound()
            Map m = this._getTorrentNotFoundMap()
            return m
        }

        Map torrentDetails = _getTorrentFileDetails(torrent.infoHash)
        String pageTitle = "ibiblio terasaur - Download details (" + torrent.name + ")"

        if (torrentDetails['fileList'] == null) {
            flash.message = "Could not find torrent file for this download"
            log.error("Missing torrent file for " + torrent.infoHash + " (" + torrent.name + ")")
        }
        [
            torrentInstance: torrent,
            torrentFileList: torrentDetails['fileList'],
            torrentAnnounceList: torrentDetails['announceList'],
            pageTitle: pageTitle,
            canEditItem: canEditItem(torrent.item),
            canonicalLink: urlHelper.canonicalLink(torrent),
            collectionLogo: getCollectionLogo(torrent.item),
        ]
    }

    private Map _getTorrentFileDetails(String infoHash) {
        Map m
        try {
            TorrentFileManager fm = this._getTorrentFileManager()
            TorrentMetainfo metainfo = fm.getTorrentMetainfoForInfoHash(infoHash)
            m = [
                fileList: metainfo.getFileList(),
                announceList: metainfo.getAnnounceList()
                ]
        } catch (FileNotFoundException e) {
            m = [
                fileList: null,
                announceList: null
                ]
        }
        return m
    }

    /**
     * Add an uploaded torrent file to an item
     */
    @Secured(['ROLE_CAN_CREATE_ITEM', 'IS_AUTHENTICATED_REMEMBERED'])
    def add = {
        Item item = null
        Map result
        boolean authFailure = false
        try {
            if (!(request instanceof MultipartHttpServletRequest)) {
                throw new TorrentControllerException('Invalid upload request')
            }

            item = getItemInstance()
            if (item) {
                result = torrentService.add(item, request)
            }
        } catch (BencodeException e) {
            flash.error = message(code:'torrent.add.error.invalid.torrent.message')
        } catch (TorrentServiceException e) {
            flash.error = e.message
        } catch (AccessDeniedException e) {
            authFailure = true
            setAccessDeniedRedirect()
        } catch (Exception e) {
            flash.error = message(code:'torrent.add.error.general.message')
            log.error("Add torrent exception (" + e.class.name + "): " + e.message, e)
        }

        if (item) {
            if (!authFailure) {
                redirect(uri: urlHelper.canonicalLink(item, 'show'))
                if (result && result.message) {
                    flash.message = result.message
                }
            }
        } else {
            redirect(controller:"browse", action: "recent")
        }
    }

    /**
     * Download torrent
     *
     * *done Check for valid torrent id (info hash)
     * *done Return error if missing/invalid info hash
     * *done Look up torrent by info hash
     * *done Return error if torrent not found
     * Verify torrent is published
     * *done Get File handle for info hash
     * *done Return error if torrent file not found
     * *done Log error if torrent file not found
     * Send headers
     * 		*done Content-Type (application/x-bittorrent)
     * 		*done Content-Length (torrent file size)
     * 		*done Content-Disposition (send torrent file as attachment)
     * 		TODO: switch to generating download links with cleaned torrent name in URL
     * 		*done Last-Modified (last mod timestamp of torrent record as "dayweek, dd mmm yyyy hh:mm:ss GMT")
     * 		Etag
     * 		*done Cache-Control max-age=7776000 (90 days)
     * 		*done Expires (now + 90 days)
     * Send binary data to response output stream
     */
    def download = {
        Torrent torrent = getTorrentInstance()
        if (!torrent) {
            this._download_sendTorrentNotFound()
            return
        }

        TorrentFileManager fm = this._getTorrentFileManager()
        File torrentFile = null
        try {
            torrentFile = fm.getFileForInfoHash(torrent.infoHash)
        } catch (FileNotFoundException e) {
            log.error("Missing torrent file for " + params.id + " (" + torrent.name + ")")
            this._download_sendTorrentNotFound()
            return
        }

        this._download_setHeaders(torrent, torrentFile)
        response.outputStream << torrentFile.newInputStream()
    }

    protected void _download_sendTorrentNotFound() {
        this._setTorrentNotFound()
        redirect(controller:"torrent", action:"show")
    }

    protected void _download_setHeaders(Torrent torrent, File torrentFile) {
        // TODO: Need to add extra headers for caching, content-length, etc.
        response.setContentLength((int)torrentFile.size())
        response.setHeader("Content-Type", "application/x-bittorrent")
        response.setHeader("Content-Disposition", "attachment;filename=\"${torrent.name}.torrent\"")

        DateTimeFormatter fmt = DateHelper.getHttpFormatter()
        DateTime expires = DateHelper.now().plusDays(90)
        response.setHeader("Last-Modified", torrent.lastUpdated.toString(fmt))
        response.setHeader("Expires", expires.toString(fmt))
        response.setHeader("Cache-Control", "max-age=7776000")

        // TODO: generate better etag w/ info hash, file last mod time, file size
        response.setHeader("Etag", this._download_generateEtag(torrent, torrentFile))
    }

    protected String _download_generateEtag(Torrent torrent, File torrentFile) {
        return torrentFile.lastModified().toString()
    }


    @Secured(['ROLE_CAN_CREATE_ITEM', 'IS_AUTHENTICATED_REMEMBERED'])
    def delete = {
        Torrent torrent = getTorrentInstance()
        Item item = null
        if (torrent) {
            item = torrent.item
        } else {
            this._setTorrentNotFound()
        }

        if (item) {
            try {
                def torrentName = torrent.name
                Map result = torrentService.delete(item, torrent)
                if (result.messageCode) {
                    flash.message = message(code:result.messageCode, args:[torrentName])
                }

                redirect(uri: urlHelper.canonicalLink(item, 'show'))
            } catch (AccessDeniedException e) {
                setAccessDeniedRedirect()
            }
        } else if (torrent) {
            redirect(uri: urlHelper.canonicalLink(torrent))
        } else {
            redirect(controller:"torrent", action:"show")
        }
    }

    /**
     * Legacy item page redirect for torrent.ibiblio.org
     */
    def legacy_redirect = {
        Torrent torrent = getTorrentInstance()
        def location = urlHelper.serverUrl(request) + urlHelper.canonicalLink(torrent)

        // controller.redirect() doesn't support 301 redirects in 1.3.x
        response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY)
        response.setHeader('Location', location)
        response.setHeader('Connection', 'close')
        render('')
        return false
    }

    /**
     * Helper functions
     */
    protected TorrentFileManager _getTorrentFileManager() {
        String torrentRoot = grailsApplication.config.terasaur.torrent.root
        if (this._torrentFileManager == null) {
            this._torrentFileManager = new TorrentFileManager(torrentRoot)
        }
        return this._torrentFileManager
    }

    protected void _setTorrentNotFound() {
        response.status = HttpServletResponse.SC_NOT_FOUND
        flash.message = message(code: 'torrent.not_found.message')
    }

    protected Map _getTorrentNotFoundMap() {
        return [
            pageTitle: message(code: 'torrent.not_found.message'),
            canEditItem: false
            ]
    }


}
